-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[Python] Set memory policy to "strict" #13593
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
7b2132c to
a2fe3d5
Compare
a2fe3d5 to
f3aa24f
Compare
f3aa24f to
b147011
Compare
b7f0c8f to
4eadd14
Compare
bb5d158 to
2141d65
Compare
d509a93 to
1833321
Compare
1833321 to
05f4dd0
Compare
05f4dd0 to
262a8b4
Compare
vepadulano
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm coming back to this old PR, I still agree with my previous statement that this is the right direction, but I see more potential misunderstandings and issues creeping up. I'm thinking about approaches we could employ to mitigate them, e.g. by running also these changes in CMSSW CI, or by thinking about more Pythonizations to include (e.g. in TList), or by thinking about if we can add more warnings for users in situations that lead to dangling pointers.
Also, a question that becomes more important now, are all of these changes still required in light of the recent and future improvements to the cppyy we use?
bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooabspdf.py
Outdated
Show resolved
Hide resolved
| # A TList is by default non-owning. To make sure that the objects live | ||
| # long enough, we attach then as an attribute of the output list, such | ||
| # that the Python reference counter doesn't hit zero. | ||
| sc.owning_pylist = sc_pylist |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds counter-intuitive for a Python user, shall we perhaps discuss making it part of a TList pythonization?
README/ReleaseNotes/v636/index.md
Outdated
| **Note:** You can change back to the old policy by calling | ||
| `ROOT.SetMemoryPolicy(ROOT.kMemoryHeuristics)` after importing ROOT, but this | ||
| should be only used for debugging purposes and this function might be removed | ||
| in the future! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should make this perhaps explicit at usage by also raising a warning from the function when calling it.
262a8b4 to
d0e71c7
Compare
|
Hi @vepadulano, thanks for coming back to this! Let's try to make progress then and merge this soon 🙂
Good points. I'll address them once we see where we stand with a fresh CI run.
The upcoming cppyy rebase on libinterop is more happening in the backend, and we have no frontend development lined up that change the behavior of these memory heuristics. But what we should consider is that it's better to not change both backend and frontend too much in one release, because it makes potential regressions harder to fix. If we assume that the backend upgrade will hit in 6.42, then this means the best time to update the memory policy is now for 6.40. Otherwise, it would be wiser to wait for 6.44, but this is quite far away. |
18dd852 to
7a08f72
Compare
|
Hi @vepadulano, I have suggested a TList Pythonization now that might address your concerns. The proposal is that when adding to owning TCollections (checked with So that means that the behavior change is only for non-owning TCollections, which now add borrowed references instead of letting the elements leak when added from Python. Is that an acceptable compromise? (no need to rush an answer, I still have to fix test failures with Python 3.14 for Fedora Rawhide 🙂 ) |
7a08f72 to
c176239
Compare
The ROOT Python interfaces have many memory leaks, which is a major pain point for people using it for long-running scripts in batch jobs. One source of memory leaks was indentified to be the "heuristic memory policy" of cppyy. This means that cppyy assumes that every non-const pointer member function argument was interpreted as the object taking ownership if the argument. For examle, take the non-owning RooLinkedList container. It has a `RooLinkedList::Add(RooAbsArg *arg)` method. ROOT wrongly assumes that this means the RooLinkedList takes ownership of arg, and it drops the ROOT overship. Nobody feels responsible for deleting the object anymore, and there is a memory leak or `arg`. That particular leak was reported in this forum post: https://root-forum.cern.ch/t/memory-leak-in-fits/56249 Function parameters of type `T *` are very common in ROOT, and only rarely do they imply ownership transfer. So changing the memory policy to "strict" would surely fix also many other memory leaks that are not reported so far. In fact, upstream cppyy doesn't even have this heuristic memory policy anymore! So moving ROOT also to the strict memory policy closes the gap between ROOT and cppyy. The potential drawback of this change are crashes in usercode if memory is not properly managed. But these problems should either be fixed by: * the user * dedicated pythonizations for these methods to manage shared ownership via Python reference counters (i.e., setting the parameter as an attribute of the object that the member function was called on) This follows up on PR root-project#4294, in particular it reverts 3a12063.
c176239 to
1fb8100
Compare
vepadulano
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I like the direction, I left a few more comments/questions.
|
|
||
| def declare_cpp_owned_arg(position, name, positional_args, keyword_args, condition=lambda _: True): | ||
| """ | ||
| Helper function to drop Python ownership of a specific funciton argument |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Helper function to drop Python ownership of a specific funciton argument | |
| Helper function to drop Python ownership of a specific function argument |
| """ | ||
| import ROOT | ||
|
|
||
| arg = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not required in Python, unless you expect sometimes the two if conditions to never trigger thus arg would not be defined. If this is a possibility, then I wonder if condition(arg) could be ill-defined, depending on whether the function condition behaves well with None
| The TMultiGraph takes ownership of the added graphs, unless we're using the | ||
| Add(TMultiGraph*) overload, in which cases it adopts the graphs inside the | ||
| other TMultiGraph. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From this phrasing it seems that also for the Add(TMultiGraph*) the TMultiGraph takes ownership, isn't it so? I don't understand if you mean that in that case the TMultiGraph is not owning the graphs.
|
|
||
| def test_getitem_slice(self): | ||
| sc = self.create_tseqcollection() | ||
| sc.SetOwner(False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact you are now calling both SetOwner(True) and SetOwner(False) in different unittests confuses me as to which is the default behaviour (in C++ and Python) of the TCollection family of classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Especially important for documentation and to address potential future user questions
| If you have a member function taking a raw pointer, like `MyClass::foo(T *obj)`, | ||
| PyROOT was so far assuming that calling this method on `my_instance` | ||
| transfers the ownership of `obj` to `my_instance`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| If you have a member function taking a raw pointer, like `MyClass::foo(T *obj)`, | |
| PyROOT was so far assuming that calling this method on `my_instance` | |
| transfers the ownership of `obj` to `my_instance`. | |
| If you have a member function taking a raw pointer, like `MyClass::foo(T *obj)`, | |
| calling such method on a Python object `my_instance` of type `MyClass` | |
| would assume that the memory ownership of `obj` transfers to `my_instance`. |
|
|
||
| The double-delete problems can be fixed via: | ||
|
|
||
| 1. Drop the ownership on the Python side with `ROOT.SetOwnership(obj, False)` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 1. Drop the ownership on the Python side with `ROOT.SetOwnership(obj, False)` | |
| 1. Dropping the ownership on the Python side with `ROOT.SetOwnership(obj, False)` |
| This affects for example the `TList::Add(TObject *obj)` member function, which | ||
| will not transfer ownership from PyROOT to the TList anymore. The new policy | ||
| fixes a memory leak, but at the same time it is not possible anymore to create | ||
| the contained elements in place: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now invalid in light of the latest Pythonizations you added, right?
| my_list = ROOT.TList() | ||
|
|
||
|
|
||
| # This is working, but resulted in memory leak prior to ROOT 6.32: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| # This is working, but resulted in memory leak prior to ROOT 6.32: | |
| # This is working, but resulted in memory leak prior to ROOT 6.40: |
| # This is not allowed anymore, as the temporary would be | ||
| # deleted immediately leaving a dangling pointer: | ||
| my_list.Add(ROOT.TObjString("obj_2")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you remind me where this is enforced in code?
| # Python reference count to contained object is now zero, | ||
| # TList contains dangling pointer! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also this is now not true thanks to the latest changes, right?
The ROOT Python interfaces have many memory leaks, which is a major pain
point for people using it for long-running scripts in batch jobs.
One source of memory leaks was indentified to be the "heuristic memory
policy" of cppyy. This means that cppyy assumes that every non-const
pointer member function argument was interpreted as the object taking
ownership if the argument.
For examle, take the non-owning RooLinkedList container. It has a
RooLinkedList::Add(RooAbsArg *arg)method. ROOT wrongly assumes thatthis means the RooLinkedList takes ownership of arg, and it drops the
ROOT overship. Nobody feels responsible for deleting the object
anymore, and there is a memory leak or
arg.That particular leak was reported in this forum post:
https://root-forum.cern.ch/t/memory-leak-in-fits/56249
Function parameters of type
T *are very common in ROOT, and onlyrarely do they imply ownership transfer. So changing the memory policy
to "strict" would surely fix also many other memory leaks that are not
reported so far. In fact, upstream cppyy doesn't even have this
heuristic memory policy anymore! So moving ROOT also to the strict
memory policy closes the gap between ROOT and cppyy.
The potential drawback of this change are crashes in usercode if memory
is not properly managed. But these problems should either be fixed by:
the user
dedicated pythonizations for these methods to manage shared
ownership via Python reference counters (i.e., setting the parameter
as an attribute of the object that the member function was called
on)
This follows up on PR #4294, in particular it reverts guitargeek@3a12063.